#!/usr/bin/env python

# Copyright (c) 2009, Purdue University
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# 
# Neither the name of the Purdue University nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Config check tool for Roster"""


__copyright__ = 'Copyright (C) 2009, Purdue University'
__license__ = 'BSD'
__version__ = '#TRUNK#'


import sys
import os
import tarfile
import shutil
import ConfigParser
import glob
import iscpy
from fabric import api as fabric_api
from fabric import state as fabric_state

from multiprocessing import Pool

from optparse import OptionParser

from roster_config_manager import config_lib
from roster_core import constants

def ChangeNamedDirectory(file_name, directory):
  """Changed the directory for named in options.

  Inputs:
    file_name: string of the filename
    directory: string of the directory
  """
  file_handle = open(file_name, 'r')
  try:
    file_contents = file_handle.readlines()
    file_handle.close()

    for line_index, line in enumerate(file_contents):
      if( ' directory "' in line ):
        line_contents = line.split(' directory "')
        new_line = '%s directory "%s"%s' % (line_contents[0],
                                            directory,
                                            line_contents[1].split('"', 1)[1])
        file_contents[line_index] = new_line
        file_handle = open(file_name, 'w')
        file_handle.writelines(file_contents)
        break
      # Also check for newline in front of directory and do same
      elif( line.startswith('directory "') ):
        line_contents = line.split('directory "')
        new_line = 'directory "%s"%s' % (directory,
                                         line_contents[1].split('"', 1)[1])
        file_contents[line_index] = new_line
        file_handle = open(file_name, 'w')
        file_handle.writelines(file_contents)
        break

    else:
      print "Couldn't find directory in named conf file"
      sys.exit(1)

  finally:
    file_handle.close()

def RunFabricNamedCheckTool(args):
  """Runs a fabric_api.local() call for named-check(conf|zone)

  Inputs:
    args: dictionary of args to be used for returning result
    from a parallel fabric named-check(conf|zone) call. ex:
      {'filename': 'root_config_dir/localhost/named/named.conf.a',
       'command': 'named-checkconf root_config_dir/localhost/named/named.conf.a'}
      -- OR --
      {'filename': 'root_config_dir/localhost/public/sub.abc.lcl.db',
       'command':
           'named-checkzone root_config_dir/localhost/public/sub.abc.lcl.db'}

  Outputs:
    dictionary of filename and named-check(conf|zone) output. ex:
      {'filename': 'root_config_dir/university.edu/named.conf.a',
       'output': ''}
  """
  filename = args['filename']
  command = args['command']
  fabric_output = fabric_api.local(command, capture=True)
  return {'filename': filename,
          'output': fabric_output}

def main(args):
  """Collects command line arguments. Checks configs.

  Inputs:
    args: list of arguments from the command line
  """
  usage = ('\n'
           '\n'
           'To check config files in an audit id:\n'
           '\t%s -i <audit-id> [--config-file <config-file>]\n'
           '\t[-z <checkzone-binary>] [-c <checkconf-binary>] [-v]\n' % sys.argv[0])

  parser = OptionParser(version='%%prog (Roster %s)' % __version__, usage=usage)

  parser.add_option('-z', '--named-checkzone', action='store',
                    dest='named_checkzone',
                    help='Location of named_checkzone binary.',
                    default='/usr/sbin/named-checkzone')
  parser.add_option('-c', '--named-checkconf', action='store',
                    dest='named_checkconf',
                    help='Location of named_checkconf binary.',
                    default='/usr/sbin/named-checkconf')
  parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
                    help='Make command verbose.', default=False)
  parser.add_option('--config-file', action='store', dest='config_file',
                    help='Config File Location', metavar='<config-file>',
                    default=constants.SERVER_CONFIG_FILE_LOCATION)
  parser.add_option('-i', '--id', dest='id',
                    help='ID of tarfile output from Roster tree export.',
                    metavar='<id>', default=None)

  (globals()['options'], args) = parser.parse_args(args)

  if( not os.path.exists(options.named_checkzone) ):
    print 'ERROR: Could not find "named-checkzone" binary.'
    sys.exit(1)
  if( not os.path.exists(options.named_checkconf) ):
    print 'ERROR: Could not find "named-checkconf" binary.'
    sys.exit(1)

  config_lib_instance = config_lib.ConfigLib(options.config_file)
  config_lib_instance.UnTarDnsTree(audit_log_id=options.id)

  try:
    err_files = []
    try:
      file_list = os.listdir(config_lib_instance.backup_dir)
    except OSError:
      print ('ERROR: Directory %s does not exist or you '
             'do not have permission.' % backup_directory)
      sys.exit(1)

    fabric_state.output['running'] = False
    fabric_state.output['warnings'] = False
    fabric_api.env.warn_only = True

    checkconf_pool = Pool(processes=config_lib_instance.max_threads)
    parallel_checkconf_args_list = []

    named_files = glob.glob('%s/*/named.conf.a' % (
        config_lib_instance.root_config_dir))
    for current_file in named_files:
      ChangeNamedDirectory(current_file, config_lib_instance.root_config_dir)
      command = '%s %s' % (options.named_checkconf, current_file)
      parallel_checkconf_args_list.append({'filename': current_file,
                                           'command': command})

    checkconf_results = checkconf_pool.map(RunFabricNamedCheckTool,
                                           parallel_checkconf_args_list)

    for result in checkconf_results:
      named_file_output = result['output']
      if( named_file_output == '' ):
        if( options.verbose ):
          print 'Finished - %s' % result['filename']
      else:
        print 'ERROR: %s' % named_file_output
        sys.exit(1)

    #Turns ['temp_dir/localhost/named.conf.a'] into ['temp_dir/localhost']
    named_dirs = [os.path.dirname(fname) for fname in named_files]

    parallel_checkzone_args_list = []
    #Check zones, one named_dir at a time
    for named_dir in named_dirs:

      #The named file for the zones we're about to check
      named_file_name = os.path.join(named_dir, 'named.conf.a')

      try:
        named_file_handle = open(named_file_name, 'r')
        named_file_string = named_file_handle.read()
      finally:
        named_file_handle.close()

      named_file_dict = iscpy.ParseISCString(named_file_string)
      global_options_dict = named_file_dict['options']
      zone_files = glob.glob('%s/*/*/*.db' % named_dir)

      checkzone_pool = Pool(processes=config_lib_instance.max_threads)

      for current_file in zone_files:
        #example - current_file = 
        #'root_config_dir/localhost/named/test_view/sub.university.lcl.db
        current_zone_file = current_file.split('/').pop()
        current_server_name = current_file.split('/')[-4]
        
        #The view name that this current_file belongs to
        view_name = os.path.dirname(current_file).split('/').pop()

        #The zones that the view name determined above, governs
        view_dict = named_file_dict['view "%s"' % view_name]

        #Finding the zone dictionary in view_dict, that is the zone_dictionary
        #for the zone represented by current_file
        zone_dict = None
        for zone in view_dict:
          #Making sure we're checking a dictionary
          if( type(view_dict[zone]) == type({}) ):
            if( 'file' in view_dict[zone].keys() ):
              if( view_dict[zone]['file'].strip('"') in current_file ):
                zone_dict = view_dict[zone]
                break
        else:
          print('Zone file %s is in the named directory %s, '
                'but not in it\'s named.conf' % (current_file, named_dir))
          sys.exit(1)

        file_handle = open(current_file, 'r')
        try:
          file_contents = file_handle.read().split('\n')
        finally:
          file_handle.close()
        for line in file_contents:
          if( line.startswith('$ORIGIN') and len(line.split()) == 2 ):
            origin = line.split()[1]
            break
        else:
          print 'ERROR: Could not find $ORIGIN in "%s"' % current_file
          sys.exit(1)

        additional_args_string = config_lib_instance.GetNamedZoneToolArgs(
            current_server_name, view_name, current_zone_file)

        #Run the named_checkzone command concurrently
        command = '%s %s %s %s' % (
        options.named_checkzone, additional_args_string, origin, current_file)

        parallel_checkzone_args_list.append({'filename': current_file,
                                             'command': command})

    results = checkzone_pool.map(RunFabricNamedCheckTool,
                                 parallel_checkzone_args_list)

    for result in results:
      checkzone_output = result['output']
      if( checkzone_output.endswith('\nOK') ):
        if( options.verbose ):
          print 'Finished - %s' % result['filename']
      else:
        print 'ERROR: %s' % checkzone_output
        sys.exit(1)

    if( options.verbose ):
      print '--------------------------------------------------------------------'
      print 'Checked %s named.conf file(s) and %s zone file(s)\n' % (
          len(named_files), len(zone_files))
      print 'All checks successful'
  finally:
    if( os.path.exists(config_lib_instance.root_config_dir) ):
      shutil.rmtree(config_lib_instance.root_config_dir)

if __name__ == '__main__':
    main(sys.argv[1:])
